package dominio.redeneural.especializacao;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import util.Util;

import dominio.ativacao.FuncaoAtivacao;
import dominio.camadaneural.CamadaNeural;
import dominio.camadaneural.especializacao.CamadaNeuralFeedForward;
import dominio.neuronio.Neuronio;
import dominio.neuronio.Sinapse;
import dominio.neuronio.especializacao.NeuronioPerceptron;
import dominio.padrao.Padrao;
import dominio.redeneural.RedeNeural;

public class FeedForward implements RedeNeural {

	private double[] estimulos;
	private double[] saidasRedeNeural;
	
	// lista de camadas da rede neural
	private List<CamadaNeural> camadasNeurais = new ArrayList<CamadaNeural>();

	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#adicionarCamadaNeural(dominio.camadaneural.CamadaNeural)
	 */
	public void adicionarCamadaNeural( CamadaNeural camadaNeural ) {
		this.camadasNeurais.add( camadaNeural );
	}

	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#removerCamadaNeural(dominio.camadaneural.CamadaNeural)
	 */
	public boolean removerCamadaNeural( CamadaNeural camadaNeural ) {
		return this.camadasNeurais.remove( camadaNeural );
	}
	
	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#estimularRedeNeural(double[])
	 */
	public double[] estimularRedeNeural( Padrao padrao ) {
		
		// gaurda uma copia para o toString
		this.estimulos = Arrays.copyOfRange(padrao.getEstimulos(), 0, padrao.getQtdeEstimulos());
		
		// 
		double[] aux = padrao.getEstimulos();
		
		/* a saida de uma camada neural eh o estimulo
		 * para de entrada para a camada seguinte 
		 */
		for(CamadaNeural c : this.camadasNeurais) {
		    aux = c.estimularCamada( new Padrao(aux) );
		}
		
		//usado no toString
		this.saidasRedeNeural = Arrays.copyOfRange(aux, 0, aux.length);

		return aux;
	}

	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#criarCamadaNeural(int, java.lang.Class, dominio.ativacao.FuncaoAtivacao)
	 */
	public void criarCamadaNeural( int quantidadeNeuronios, Class<? extends Neuronio> tipoNeuronio, FuncaoAtivacao funcaoAtivacao ) {
		CamadaNeural camadaNeural = new CamadaNeuralFeedForward();
		
		for(int i = 0; i < quantidadeNeuronios; i++) {
			Neuronio neuronio = criarNeuronio(tipoNeuronio);
			neuronio.definirFuncaoAtivacao( funcaoAtivacao );
		    camadaNeural.adicionarNeuronio(neuronio);
		}
		
		this.camadasNeurais.add(camadaNeural);
	}

	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#criarNeuronio(java.lang.Class)
	 */
	public Neuronio criarNeuronio( Class<? extends Neuronio> tipoNeuronio ) {
		Neuronio neuronio = null;
		
		try {
		    neuronio = tipoNeuronio.newInstance();
		} catch (InstantiationException e) {
		    e.printStackTrace();
		} catch (IllegalAccessException e) {
		    e.printStackTrace();
		} 

		return neuronio;
	}

	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#criarCamadasNeurais(int, int[], java.lang.Class, dominio.ativacao.FuncaoAtivacao[])
	 */
	public void criarCamadasNeurais(int quantidadeCamadas, int[] quantidadeNeuronios, Class<? extends Neuronio> tipoNeuronio, FuncaoAtivacao[] funcoesAtivacao) throws IllegalArgumentException {
		
		if(quantidadeCamadas != quantidadeNeuronios.length || quantidadeCamadas != funcoesAtivacao.length) {
		    throw new IllegalArgumentException("A quantidade de camadas deve ser igual a quantidade de elementos dos vetores de quantidade de neuronios e tipo de funcao de ativacao!");
		}
		
		for(int i = 0; i < quantidadeCamadas; i++) {
		    criarCamadaNeural(quantidadeNeuronios[i], tipoNeuronio, funcoesAtivacao[i]);
		}		
	}
	
	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#treinarRedePerceptron(double[][], double[][])
	 */
	public void treinarRedePerceptron(List<Padrao> padroesTreinamento, double[][] dy) {
		
		CamadaNeural camadaNeural = this.camadasNeurais.get(0);
		List<Neuronio> neuronios = camadaNeural.getNeuronios();
		
		double E = 1;
		double txAprendizagem = 0.2;
		int epoca = 0;
		int qtdeMaxIteracoes = 100;
		
		double[] e = new double[padroesTreinamento.size()];
		double[] y = new double[padroesTreinamento.size()];
		
		int countPadrao = 0;
		for (Neuronio neuronio : neuronios) {
			neuronio.criarSinapses( padroesTreinamento.get(countPadrao++) );
		}
		
		while(epoca < qtdeMaxIteracoes && E > 0){
			
			//para cada padrao de entrada
			for (int i = 0; i < padroesTreinamento.size(); i++) {
				
				Padrao padrao = padroesTreinamento.get(i);
				
				//
				int cneuron = 0;
				for (Neuronio n : neuronios) {
					y[i] = n.estimular( padrao );
					e[cneuron] = dy[i][cneuron] - y[i]; 
					cneuron++;
				}
				
				cneuron = 0;
				for (Neuronio n : neuronios) {
					
					Collection<Sinapse> sinapses = n.getSinapses();
					for (Sinapse s : sinapses) {
						s.ajustarPeso( txAprendizagem * e[cneuron] * s.getEntrada() );
					}
					
					NeuronioPerceptron np = ((NeuronioPerceptron) n);
					np.setBias( np.getBias() + txAprendizagem * e[cneuron] );
					
					cneuron++;
				}
			}
			
			E = Util.erroQuadratico(e);
			epoca++;
		}
		
	}
	
	/* (non-Javadoc)
	 * @see dominio.redeneural.RedeNeural#autoOrganizar(java.util.List)
	 */
	public void autoOrganizar(List<Padrao> padroesEntrada) {
		
		CamadaNeural camadaNeural = this.camadasNeurais.get(0);
		List<Neuronio> neuronios = camadaNeural.getNeuronios();
		
		int countPadrao = 0;
		for (Neuronio neuronio : neuronios) {
			neuronio.criarSinapses( padroesEntrada.get(countPadrao++) );
		}
		
		double txAprendizagemInicial = 0.1;
		double txAprendizagem = txAprendizagemInicial;
		double t2 = 1000;
		double distMinima = 0.0001;
		
		int epoca = 1;
		int qtdeMaxIteracoes = 1000;

		//vetor de saida Y para cada neuronio
		double[] yK = new double[padroesEntrada.size()];
		
		organizacao:
		while(epoca < qtdeMaxIteracoes){
			
			/* para cada padrao de entrada */
			for(int i=0; i < padroesEntrada.size(); i++){
				
				/* para cada neuronio */
				for(int j=0; j < padroesEntrada.size(); j++){ 
					Neuronio n = neuronios.get( j );
					NeuronioPerceptron np = (NeuronioPerceptron) n;

					yK[j] = Util.getDistanciaEuclidiana( np );
				}
				
				int indiceVencedor = Util.getVencedorCompeticao( yK );
				
				if(yK[indiceVencedor] < distMinima){
					System.out.println( "" + yK[indiceVencedor] + " - " + distMinima);
					System.out.println( "parouu... t = " + epoca );
					break organizacao;
				}

				//atualiza os pesos do neuronio vencedor
				Neuronio n = neuronios.get( indiceVencedor );
				List<Sinapse> sinapses = n.getSinapses();
				for( Sinapse s : sinapses ) {
					s.ajustarPeso( txAprendizagem * (s.getEntrada() - s.getPeso()) );
				}
			}
			
			txAprendizagem = txAprendizagemInicial * Math.exp( -epoca / t2 );
			epoca++;
		}	
		
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		DecimalFormat formatter = new DecimalFormat( "0.0000" );
		formatter.setPositivePrefix( " " );
		
		StringBuffer buf = new StringBuffer();
		buf.append( "Rede Neural Feed Forward ############################################################\n\n" );
		
		for (CamadaNeural camadaNeural : this.camadasNeurais) {
			buf.append( camadaNeural.toString() );
		}
		
		buf.append( "\tResumo da Rede Neural #######################################################\n" );
		buf.append( "\tEstimulos: " );
		if( this.estimulos != null ){
			for (int i = 0; i < this.estimulos.length; i++) {
				buf.append( formatter.format( this.estimulos[i] ) ).append( i < this.estimulos.length - 1 ? ", " : "\n" );
			}
		}else{
			buf.append( "NENHUM" ).append( "\n" );
		}
		buf.append( "\tSaidas da Rede: " );
		if(this.saidasRedeNeural != null){
			for (int i = 0; i < this.saidasRedeNeural.length; i++) {
				buf.append( formatter.format( this.saidasRedeNeural[i] ) ).append( i < this.saidasRedeNeural.length - 1 ? ", " : "\n\n" );
			}
		}else{
			buf.append( "NENHUM" ).append( "\n" );
		}
		buf.append( "#####################################################################################\n" );
		return buf.toString();
	}

}
